Skip to Content

总的来说,创建新进程主要有两种主流模型:

  1. Unix/Linux 模型: 分两步走,即 fork() + exec()
  2. Windows 模型: 一步到位,即 CreateProcess()

下面我们分别详细介绍这两种模型。


1. Unix/Linux 模型: fork()exec()

在 Unix、Linux 以及其他类 Unix 系统(如 macOS)中,创建新进程是一个“两步走”的过程。这种设计非常优雅且灵活。

第一步: fork() - 克隆进程

fork() 系统调用的作用是创建一个当前进程的副本(克隆)

  • 工作方式: 当一个进程(称为“父进程”)调用 fork() 时,操作系统内核会:

    1. 为新进程(称为“子进程”)分配一个新的、唯一的进程ID(PID)。
    2. 创建一个新的进程控制块(PCB),并复制父进程PCB的大部分内容。
    3. 复制父进程的整个地址空间(包括代码、数据、堆栈)。
    4. 复制父进程打开的文件描述符、环境变量等。
  • 关键特性 - 写时复制 (Copy-on-Write, COW): 早期的 fork() 实现会完整地复制整个内存空间,效率较低。现代操作系统普遍采用 写时复制(COW) 技术进行优化。这意味着,fork() 之后,父子进程在逻辑上拥有独立的内存空间,但物理上它们共享相同的内存页。只有当其中一个进程尝试写入某个内存页时,内核才会真正为该进程复制一份这个内存页,让它拥有自己的副本。这极大地提高了 fork() 的效率,因为大多数情况下,子进程很快会调用 exec(),之前的内存复制就白费了。

  • fork() 的返回值: fork() 的返回值非常巧妙,是区分父子进程的关键:

    • 父进程中,fork() 返回新创建的子进程的PID(一个正整数)。
    • 子进程中,fork() 返回 0
    • 如果创建失败,fork() 在父进程中返回 -1

代码示例 (fork()):

#include <stdio.h> #include <unistd.h> #include <sys/types.h> int main() { pid_t pid = fork(); // 创建子进程 if (pid < 0) { // fork 失败 fprintf(stderr, "Fork Failed\n"); return 1; } else if (pid == 0) { // 这是子进程执行的代码 printf("I am the child process, my PID is %d\n", getpid()); } else { // 这是父进程执行的代码 printf("I am the parent process, my PID is %d, my child's PID is %d\n", getpid(), pid); } // 父子进程都会执行这里的代码 printf("This line is executed by both processes.\n"); return 0; }

第二步: exec() - 加载新程序

fork() 只是创建了一个父进程的副本,如果想让子进程执行一个全新的程序,就需要 exec() 系列函数。

  • 工作方式: exec() 系列函数会用一个新的程序替换当前进程的内存空间(包括代码、数据和堆栈)。

    • 进程的 PID 保持不变
    • 一旦 exec() 调用成功,它永远不会返回。新的程序会从它的 main 函数开始执行。如果 exec() 返回了,那一定是出错了。
  • exec() 函数家族: exec() 不是一个函数,而是一组函数,它们的细微差别在于参数传递方式和是否使用系统 PATH 环境变量来查找可执行文件。

    • execl(path, arg0, arg1, ...): 参数以列表形式传入。
    • execv(path, argv[]): 参数以字符串数组(vector)形式传入。
    • execlp(...), execvp(...): p 代表会自动在 PATH 环境变量中搜索可执行文件。
    • execle(...), execve(...): e 代表可以手动传入新的环境变量。

fork() + exec() 组合使用

这才是创建新进程并运行新程序的标准模式。典型的应用场景就是 Shell(命令行解释器)。

  1. Shell(父进程)调用 fork() 创建一个子进程。
  2. 子进程调用 execvp() 来执行用户输入的命令(例如 ls -l)。
  3. 父进程(Shell)通常会调用 wait()waitpid() 等待子进程执行结束,然后返回到命令提示符,等待下一个命令。

代码示例 (fork() + execvp())

#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main() { pid_t pid = fork(); if (pid < 0) { fprintf(stderr, "Fork Failed\n"); return 1; } else if (pid == 0) { // 子进程 printf("Child process is about to run 'ls -l'\n"); char *args[] = {"ls", "-l", NULL}; // execvp 的参数数组,必须以 NULL 结尾 execvp(args[0], args); // 加载并执行 ls 命令 // 如果 execvp 成功,下面的代码永远不会被执行 perror("execvp failed"); // 如果执行到这里,说明 execvp 出错了 return 1; } else { // 父进程 printf("Parent process is waiting for the child to complete...\n"); wait(NULL); // 等待子进程结束 printf("Child process has finished.\n"); } return 0; }

2. Windows 模型: CreateProcess()

Windows 采用了一种更“直接”或“一体化”的方式来创建进程,通过一个功能强大的 API 函数 CreateProcess() 来完成。

  • 工作方式: CreateProcess() 函数一步到位地完成了进程创建程序加载两项任务。它不会像 fork() 那样复制父进程的上下文。

    • 它会创建一个全新的、独立的进程。
    • 然后将指定的可执行文件加载到这个新进程的地址空间中。
    • 父进程会得到新进程和其主线程的句柄(Handle),以便后续进行管理和同步。
  • CreateProcess() 函数: 这个函数的参数非常多(有10个),提供了非常精细的控制,例如:

    • 要执行的程序名和命令行参数。
    • 进程和线程的安全属性。
    • 环境变量。
    • 进程的启动信息(如窗口如何显示)。
    • 返回新创建进程和主线程的句柄。

简化的 CreateProcess() 概念性代码示例 (C++):

#include <windows.h> #include <stdio.h> int main() { STARTUPINFO si; PROCESS_INFORMATION pi; // 初始化结构体 ZeroMemory(&si, sizeof(si)); si.cb = sizeof(si); ZeroMemory(&pi, sizeof(pi)); // 要执行的命令 // 注意:在 Windows 中,字符串最好使用 TCHAR 类型以支持 Unicode TCHAR cmd[] = TEXT("C:\\Windows\\System32\\notepad.exe"); // 创建新进程 if (!CreateProcess( NULL, // 不使用模块名 cmd, // 命令行字符串 NULL, // 进程安全属性 NULL, // 线程安全属性 FALSE, // 句柄不被继承 0, // 创建标志 NULL, // 使用父进程的环境变量 NULL, // 使用父进程的当前目录 &si, // 指向 STARTUPINFO 结构的指针 &pi // 指向 PROCESS_INFORMATION 结构的指针 )) { printf("CreateProcess failed (%d).\n", GetLastError()); return 1; } printf("Process created with PID: %d\n", pi.dwProcessId); // 等待子进程结束 WaitForSingleObject(pi.hProcess, INFINITE); // 关闭进程和线程句柄 CloseHandle(pi.hProcess); CloseHandle(pi.hThread); printf("Child process has finished.\n"); return 0; }

总结与对比

特性Unix/Linux (fork() + exec())Windows (CreateProcess())
设计哲学两步分离:创建(克隆)和执行(替换)是分开的。一步到位:创建和执行合并在一个函数调用中。
灵活性。子进程可以在调用 exec() 之前修改其环境(如重定向文件描述符、更改环境变量),这对于实现Shell的I/O重定向等功能非常方便。较低。虽然参数众多,但子进程在执行新程序代码前能做的事情有限。
效率看起来低效(复制整个地址空间),但**写时复制(COW)**技术使其在大多数情况下非常高效。概念上更直接高效,因为它直接创建新进程并加载程序,无需复制父进程。
继承性子进程默认继承父进程的大部分资源(内存、文件描述符等)。子进程不继承父进程的地址空间。其他资源(如句柄)是否继承可以通过参数精确控制。
复杂度概念简单,两个函数的职责清晰。API 复杂,单个函数有10个参数,需要精细配置。

总而言之,这两种模型都有效地完成了创建新进程的任务,但它们的设计哲学反映了各自操作系统的历史和设计目标。Unix 的 fork/exec 模型因其简洁和强大的灵活性而备受赞誉,而 Windows 的 CreateProcess 则提供了一种功能丰富、控制精细的“一站式”解决方案。

Last updated on